package msgpstore

import (
	"compress/gzip"
	"io"
	"os"
	"regexp"
	"strings"
	"sync"
	"github.com/neovim/go-client/msgpack"
	"path/filepath"
	"github.com/fsnotify/fsnotify"
	"log"
	"errors"
)

// NoSuchKeyError is thrown when calling Get with invalid key
type NoSuchKeyError struct {
	key string
}

func (err NoSuchKeyError) Error() string {
	return "msgpstore: no such key \"" + err.key + "\""
}

type SysConf map[string]string

// MsgpStore is the basic store object.
type MsgpStore struct {
	Data SysConf
	sync.RWMutex
	DbPath string
	onConfigChange func(fsnotify.Event)
}

// Open will load a msgpstore from a file.
func Open(filename string) (*MsgpStore, error) {
	var toOpen = make(SysConf)
	LoadMapData(filename, &toOpen)

	ks := new(MsgpStore)
	ks.DbPath = filename
	ks.Data = make(SysConf)
	for key := range toOpen {
		ks.Data[key] = toOpen[key]
	}
	return ks, nil
}

// Save writes the msgpstore to disk
func Save(ks *MsgpStore, filename string) (err error) {
	ks.RLock()
	defer ks.RUnlock()
	toSave := make(SysConf)
	for key := range ks.Data {
		toSave[key] = string(ks.Data[key])
	}
	f, err := os.Create(filename)
	if err != nil {
		return err
	}
	defer f.Close()
	if strings.HasSuffix(filename, ".gz") {
		w := gzip.NewWriter(f)
		defer w.Close()
		return msgpack.NewEncoder(w).Encode(toSave)
	}
	return msgpack.NewEncoder(f).Encode(toSave)
}

func LoadMapData(filename string, m *SysConf) error {
	var err error
	f, err := os.Open(filename)
	defer f.Close()
	if err != nil {
		return err
	}

	var w io.Reader
	if strings.HasSuffix(filename, ".gz") {
		w, err = gzip.NewReader(f)
		if err != nil {
			return err
		}
	} else {
		w = f
	}

	toOpen := make(SysConf)
	err = msgpack.NewDecoder(w).Decode(toOpen)
	if err != nil {
		return err
	}
	*m = toOpen
	return nil
}

func (s *MsgpStore) Reload() error {
	filename := s.DbPath
	var toOpen = make(SysConf)
	LoadMapData(filename, &toOpen)
	for key := range toOpen {
		s.Data[key] = toOpen[key]
	}
	return nil
}

// Set saves a value at the given key.
func (s *MsgpStore) Set(key string, value string) error {
	s.Lock()
	defer s.Unlock()
	if s.Data == nil {
		s.Data = make(SysConf)
	}
	s.Data[key] = value
	return nil
}

// Get will return the value associated with a key.
func (s *MsgpStore) Get(key string, value *string) error {
	s.RLock()
	defer s.RUnlock()
	v, ok := s.Data[key]
	if !ok {
		return NoSuchKeyError{key}
	}
	*value = v
	return nil
}

// GetAll is like a filter with a regexp. If the regexp is nil, then it returns everything.
func (s *MsgpStore) GetAll(re *regexp.Regexp) SysConf {
	s.RLock()
	defer s.RUnlock()
	results := make(SysConf)
	for k, v := range s.Data {
		if re == nil || re.MatchString(k) {
			results[k] = v
		}
	}
	return results
}

// Keys returns all the keys currently in map
func (s *MsgpStore) Keys() []string {
	s.RLock()
	defer s.RUnlock()
	keys := make([]string, len(s.Data))
	i := 0
	for k := range s.Data {
		keys[i] = k
		i++
	}
	return keys
}

// Delete removes a key from the store.
func (s *MsgpStore) Delete(key string) {
	s.Lock()
	defer s.Unlock()
	delete(s.Data, key)
}

func (s *MsgpStore) GetDbPath() (string, error) {
	if s.DbPath == "" {
		return "", errors.New("DbPath has not been set.")
	}
	return s.DbPath,nil
}

func (v *MsgpStore) WatchDb() {
	go func() {
		watcher, err := fsnotify.NewWatcher()
		if err != nil {
			log.Fatal(err)
		}
		defer watcher.Close()

		// we have to watch the entire directory to pick up renames/atomic saves in a cross-platform way
		filename, err := v.GetDbPath()
		if err != nil {
			log.Println("error:", err)
			return
		}

		configFile := filepath.Clean(filename)
		configDir, _ := filepath.Split(configFile)

		done := make(chan bool)
		go func() {
			for {
				select {
				case event := <-watcher.Events:
					// we only care about the config file
					if filepath.Clean(event.Name) == configFile {
						if event.Op&fsnotify.Write == fsnotify.Write || event.Op&fsnotify.Create == fsnotify.Create {
							err := v.Reload()
							if err != nil {
								log.Println("error:", err)
							}
							v.onConfigChange(event)
						}
					}
				case err := <-watcher.Errors:
					log.Println("error:", err)
				}
			}
		}()

		watcher.Add(configDir)
		<-done
	}()
}

func (v *MsgpStore) OnDbChange(run func(in fsnotify.Event)) {
	v.onConfigChange = run
}